<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>模拟 vue 响应式原理</title>
  </head>
  <body>
    <script>
      // 数据类型检测
      function detectionDataType(targetObj) {
        return Object.prototype.toString.call(targetObj).slice(8, -1);
      }

      // 数据代理
      function dataBrokers(targetObj, proxyObj) {
        for (let key of Object.keys(targetObj)) {
          Object.defineProperty(proxyObj, key, {
            get() {
              return targetObj[key];
            },
            set(newVal) {
              targetObj[key] = newVal;
            },
          });
        }
      }

      // 数据劫持（当前数据）
      function dataHijackedCurrent(targetObj, attribute, val) {
        // 对 val 进行深度劫持
        dataHijackedInside(val);
        // 为每一个响应式数据添加 Dep 用于，收集依赖，通知更新
        const dep = new Dep();
        Object.defineProperty(targetObj, attribute, {
          get() {
            // 判断是否需要收集 watcher 依赖（并不是所有的读取数据都是通过插值语法（实例化 Watcher），比如在方法值使用数据）
            if (Dep.watcher) {
              // 调用 Dep 收集依赖
              dep.depend(Dep.watcher);
            }
            return val;
          },
          set(newVal) {
            val = newVal;
            // 数据更改完成后，调用 Dep 通知更新
            dep.notify();
          },
        });
      }
      // 数据劫持（内部数据）
      function dataHijackedInside(targetObj) {
        // 判断 val 的值是否可以进行深度劫持
        if (
          detectionDataType(targetObj) !== "Object" &&
          detectionDataType(targetObj) !== "Array"
        )
          return;
        /* for (let [key, value] of Object.entries(targetObj)) {
          deep(targetObj, key, value);
        } */
        // 封装一个观察类，帮我们分别处理 Object 和 Array 的数据劫持
        new Observe(targetObj);
      }

      // 观察类（处理 Object 和 Array 不同的数据代理）
      class Observe {
        constructor(targetObj) {
          // 判读 targetObj 是数组还是对象
          if (Array.isArray(targetObj)) {
            this.observeArray(targetObj);
          } else {
            this.walk(targetObj);
          }
        }
        // 在 Observe 的原型对象上添加一个用于观察数组进行数据劫持的方法
        observeArray(targetObj) {
          console.log("重写数组原型对象上的 push、pop ... 方法。");
          // 对数组中的值进行劫持（数组不进行数据劫持直接劫持数组内部数据）
          targetObj.forEach((item) => {
            dataHijackedInside(item);
          });
        }

        // 在 Observe 的原型对象上添加一个用于观察对象进行数据劫持的方法
        walk(targetObj) {
          for (let [key, value] of Object.entries(targetObj)) {
            // 对对象进行数据劫持
            dataHijackedCurrent(targetObj, key, value);
          }
        }
      }

      // Dep 类，用来收集不同数据的依赖（watcher），通知依赖更新
      class Dep {
        constructor() {
          // 用于存放该数据数据所有 watcher
          this.subs = [];
        }
        // 收集依赖
        depend(watcher) {
          this.subs.push(watcher);
          console.log(
            "每次访问我的数据的时候,我就把watcher收集到dep.subs这个数组中",
            this.subs
          );
        }
        // 通知更新
        notify() {
          console.log("通知所有的watcher 重新加载新的数据");
          this.subs.forEach((watcher) => watcher.update());
        }
      }

      // Watcher 类，模板中用到数据时会实例化 Watcher 依赖
      class Watcher {
        constructor(targetObj, attribute) {
          // 将属性添加到实例对象上供方法使用
          this.targetObj = targetObj;
          this.attribute = attribute;
          // 将 Watcher 的实例对象，添加到 Dep 静态属性上（让每一个 Dep 实例都能使用）
          Dep.watcher = this;
          // 调用 get 读取数据
          this.$val = this.get();
          // 将 Dep 静态属性清空（该依赖已经添加完毕）
          Dep.watcher = null;
        }
        // 用于获取数据（插值语法的目的包含得到一个值和生成这个数据在这个插值语法的依赖对象）
        get() {
          // 读取 _data 上的值会触发对应数据的 get 由 get 中对应的 Dep 帮我们收集 Watcher
          return this.targetObj[this.attribute];
        }
        //在watcher内部封装一个update方法,当通知我更新的时候,直接调用当前的update方法
        update() {
          console.log(
            "gogogo,有人通知我了,我重新去请求数据是:" +
              this.get() +
              ",然后开始重新渲染视图!!!!"
          );
        }
      }
      // 1.创建 vue 构造函数
      function Vue(options) {
        // 2.将配置对象中的 data 赋值给 vm 上的 _data 属性
        switch (detectionDataType(options.data)) {
          case "Object":
            this._data = options.data;
            break;
          case "Function":
            this._data = options.data();
            break;
          default:
            throw new Error("Vue 配置对象中的 data 应该为一个对象或方法！！！");
        }
        // 3.代理 _data 中的数据到 vm 上（只代理第一层）
        dataBrokers(this._data, this);
        // 4.劫持 _data 中的所有数据（自己代理自己）
        dataHijackedInside(this._data);
      }

      // 实例化 Vue
      const vm = new Vue({
        data() {
          return {
            name: "张三",
            age: 20,
            friends: [
              { name: "土豪1", age: "3" },
              { name: "土豪2", age: "6" },
              1,
              2,
            ],
            hobby: ["抽烟", "喝酒", "烫头"],
          };
        },
      });

      function read() {
        console.log(new Watcher(vm, "name").$val);
      }
      read();
    </script>
  </body>
</html>
